package widgets;

import util.ArrayUtil;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

/**
 * This is a replacement for {@link Popup}, and is heavily based off of {@link Popup}, {@link PopupFactory.LightWeightPopup},
 * and {@link JPopupMenu}, only much simpler and with much less unnecessary bloat.
 *
 * The major advantage of this is that it allows for transparent contents to be displayed properly.
 *
 * @author Neil Dickson
 */
public class CustomPopup {
	/**
	 * Component we are to be added to.
	 */
	private Component owner;
	/**
	 * Component holding the contents of the popup
	 */
	private Component contents;
	/**
	 * Desired location
	 */
	private Point location;

	/**
	 * This {@link MouseListener} simply blocks {@link MouseEvent}s from going to components below.
	 */
	public static final MouseListener	BLOCKING_MOUSE_LISTENER	= new MouseListener() {
		public void mouseClicked(MouseEvent e) {
			e.consume();
		}
		public void mousePressed(MouseEvent e) {
			e.consume();
		}
		public void mouseReleased(MouseEvent e) {
			e.consume();
		}
		public void mouseEntered(MouseEvent e) {
			e.consume();
		}
		public void mouseExited(MouseEvent e) {
			e.consume();
		}
	};
	/**
	 * This {@link KeyListener} simply blocks {@link KeyEvent}s from going to components below.
	 */
	public static final KeyListener		BLOCKING_KEY_LISTENER = new KeyListener() {
		public void keyTyped(KeyEvent e) {
			e.consume();
		}
		public void keyPressed(KeyEvent e) {
			e.consume();
		}
		public void keyReleased(KeyEvent e) {
			e.consume();
		}
	};

	public CustomPopup(Component owner, Component contents, Point location) {
		if ((owner instanceof JFrame) || (owner instanceof JDialog) || (owner instanceof JWindow)) {
			// Force the content to be added to the layered pane, otherwise we'll get an exception when adding to the RootPaneContainer.
			owner = ((RootPaneContainer) owner).getLayeredPane();
		}
		this.location = location;
		this.owner = owner;
		this.contents = contents;
		// NOTE: One must force all mouse and keyboard events to be consumed before reaching lower components, otherwise they receive everything, even through the contents.
		setBlocking(true);
	}

	public void setBlocking(boolean blocking) {
		if (blocking) {
			if (!ArrayUtil.containsExact(contents.getMouseListeners(),BLOCKING_MOUSE_LISTENER)) {
				contents.addMouseListener(BLOCKING_MOUSE_LISTENER);
			}
			if (!ArrayUtil.containsExact(contents.getKeyListeners(),BLOCKING_KEY_LISTENER)) {
				contents.addKeyListener(BLOCKING_KEY_LISTENER);
			}
		}
		else {
			contents.removeMouseListener(BLOCKING_MOUSE_LISTENER);
			contents.removeKeyListener(BLOCKING_KEY_LISTENER);
		}
	}

	public void hide() {
		if (contents != null) {
			Container parent = contents.getParent();
			if (parent != null) {
				Rectangle bounds = contents.getBounds();
				parent.remove(contents);
				parent.repaint(bounds.x, bounds.y, bounds.width,bounds.height);
			}
		}
		owner = null;
	}

	public void show() {
		Container parent = null;

		if (owner != null) {
			parent = (owner instanceof Container ? (Container) owner : owner.getParent());
		}

		// Try to find a JLayeredPane and Window to add
		for (Container p = parent; p != null; p = p.getParent()) {
			if (p instanceof JRootPane && !(p.getParent() instanceof JInternalFrame)) {
				parent = ((JRootPane) p).getLayeredPane();
				// Continue, so that if there is a higher JRootPane, we'll pick it up.
			}
			else if (p instanceof Window) {
				if (parent == null) {
					parent = p;
				}
				break;
			}
			else if (p instanceof JApplet) {
				// Painting code stops at Applets, we don't want to add to a Component above an Applet otherwise you'll never see it painted.
				break;
			}
		}

		Point p = null;
		for (Container c = parent; c != null; c = c.getParent()) {
			if (c instanceof Window) {
				Point point = new Point(location);
				SwingUtilities.convertPointFromScreen(point, parent);
				p = point;
			}
		}
		if (p != null) {
			adjustPopupLocationToFitScreen(p);
			contents.setLocation(p);
			if (parent instanceof JLayeredPane) {
				parent.add(contents, JLayeredPane.POPUP_LAYER, 0);
			}
			else {
				parent.add(contents);
			}
		}
	}

	/**
	 * Adjusts a {@link java.awt.Point} to take into account the desktop bounds, taskbar and multi-monitor configuration.
	 *
	 * @param p the Point to be adjusted
	 */
	private void adjustPopupLocationToFitScreen(Point p) {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		Rectangle screenBounds;
		Insets screenInsets;
		GraphicsConfiguration gc = null;
		// Try to find GraphicsConfiguration, that includes mouse pointer position
		GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsDevice[] graphicsDevices = graphicsEnvironment.getScreenDevices();
		for (GraphicsDevice device : graphicsDevices) {
			if (device.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
				GraphicsConfiguration dgc = device.getDefaultConfiguration();
				if (dgc.getBounds().contains(p)) {
					gc = dgc;
					break;
				}
			}
		}

		// If not found and we have invoker, ask invoker about his gc
		if (gc==null && owner!=null) {
			gc = owner.getGraphicsConfiguration();
		}

		if (gc != null) {
			// If we have GraphicsConfiguration use it to get screen bounds and insets
			screenInsets = toolkit.getScreenInsets(gc);
			screenBounds = gc.getBounds();
		}
		else {
			// If we don't have GraphicsConfiguration use primary screen and empty insets
			screenInsets = new Insets(0, 0, 0, 0);
			screenBounds = new Rectangle(toolkit.getScreenSize());
		}

		int scrWidth = screenBounds.width - Math.abs(screenInsets.left + screenInsets.right);
		int scrHeight = screenBounds.height - Math.abs(screenInsets.top + screenInsets.bottom);

		Dimension size = contents.getSize();

		if ((p.x + size.width) > screenBounds.x + scrWidth) {
			p.x = screenBounds.x + scrWidth - size.width;
		}
		if ((p.y + size.height) > screenBounds.y + scrHeight) {
			p.y = screenBounds.y + scrHeight - size.height;
		}

		// Change is made to the desired (x,y) values, when the PopupMenu is too tall OR too wide for the screen
		if (p.x < screenBounds.x) {
			p.x = screenBounds.x;
		}
		if (p.y < screenBounds.y) {
			p.y = screenBounds.y;
		}
	}
}
